'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * Module dependencies.
 */
const contentDisposition = require("content-disposition");
const ensureErrorHandler = require("error-inject");
const getType = require("cache-content-type");
const onFinish = require("on-finished");
const isJSON = require("koa-is-json");
const escape = require("escape-html");
const statuses = require("statuses");
const destroy = require("destroy");
const assert = require("assert");
const vary = require("vary");
const only = require("only");
const util = require("util");
const extname = require('path').extname;
const typeis = require('type-is').is;
/**
 * Prototype.
 */
exports = {
    /**
     * Return the request socket.
     *
     * @return {Connection}
     * @api public
     */
    get socket() {
        return this.res.socket;
    },
    /**
     * Return response header.
     *
     * @return {Object}
     * @api public
     */
    get header() {
        const { res } = this;
        return typeof res.getHeaders === 'function'
            ? res.getHeaders()
            : res._headers || {}; // Node < 7.7
    },
    /**
     * Return response header, alias as response.header
     *
     * @return {Object}
     * @api public
     */
    get headers() {
        return this.header;
    },
    /**
     * Get response status code.
     *
     * @return {Number}
     * @api public
     */
    get status() {
        return this.res.statusCode;
    },
    /**
     * Set response status code.
     *
     * @param {Number} code
     * @api public
     */
    set status(code) {
        if (this.headerSent)
            return;
        assert(Number.isInteger(code), 'status code must be a number');
        assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
        this._explicitStatus = true;
        this.res.statusCode = code;
        if (this.req.httpVersionMajor < 2)
            this.res.statusMessage = statuses[code];
        if (this.body && statuses.empty[code])
            this.body = null;
    },
    /**
     * Get response status message
     *
     * @return {String}
     * @api public
     */
    get message() {
        return this.res.statusMessage || statuses[this.status];
    },
    /**
     * Set response status message
     *
     * @param {String} msg
     * @api public
     */
    set message(msg) {
        this.res.statusMessage = msg;
    },
    /**
     * Get response body.
     *
     * @return {Mixed}
     * @api public
     */
    get body() {
        return this._body;
    },
    /**
     * Set response body.
     *
     * @param {String|Buffer|Object|Stream} val
     * @api public
     */
    set body(val) {
        const original = this._body;
        this._body = val;
        // no content
        if (null == val) {
            if (!statuses.empty[this.status])
                this.status = 204;
            this.remove('Content-Type');
            this.remove('Content-Length');
            this.remove('Transfer-Encoding');
            return;
        }
        // set the status
        if (!this._explicitStatus)
            this.status = 200;
        // set the content-type only if not yet set
        const setType = !this.header['content-type'];
        // string
        if ('string' == typeof val) {
            if (setType)
                this.type = /^\s*</.test(val) ? 'html' : 'text';
            this.length = Buffer.byteLength(val);
            return;
        }
        // buffer
        if (Buffer.isBuffer(val)) {
            if (setType)
                this.type = 'bin';
            this.length = val.length;
            return;
        }
        // stream
        if ('function' == typeof val.pipe) {
            onFinish(this.res, destroy.bind(null, val));
            ensureErrorHandler(val, (err) => this.ctx.onerror(err));
            // overwriting
            if (null != original && original != val)
                this.remove('Content-Length');
            if (setType)
                this.type = 'bin';
            return;
        }
        // json
        this.remove('Content-Length');
        this.type = 'json';
    },
    /**
     * Set Content-Length field to `n`.
     *
     * @param {Number} n
     * @api public
     */
    set length(n) {
        this.set('Content-Length', n);
    },
    /**
     * Return parsed response Content-Length when present.
     *
     * @return {Number}
     * @api public
     */
    get length() {
        const len = this.header['content-length'];
        const body = this.body;
        if (null == len) {
            if (!body)
                return;
            if ('string' == typeof body)
                return Buffer.byteLength(body);
            if (Buffer.isBuffer(body))
                return body.length;
            if (isJSON(body))
                return Buffer.byteLength(JSON.stringify(body));
            return;
        }
        return Math.trunc(len) || 0;
    },
    /**
     * Check if a header has been written to the socket.
     *
     * @return {Boolean}
     * @api public
     */
    get headerSent() {
        return this.res.headersSent;
    },
    /**
     * Vary on `field`.
     *
     * @param {String} field
     * @api public
     */
    vary(field) {
        if (this.headerSent)
            return;
        vary(this.res, field);
    },
    /**
     * Perform a 302 redirect to `url`.
     *
     * The string "back" is special-cased
     * to provide Referrer support, when Referrer
     * is not present `alt` or "/" is used.
     *
     * Examples:
     *
     *    this.redirect('back');
     *    this.redirect('back', '/index.html');
     *    this.redirect('/login');
     *    this.redirect('http://google.com');
     *
     * @param {String} url
     * @param {String} [alt]
     * @api public
     */
    redirect(url, alt) {
        // location
        if ('back' == url)
            url = this.ctx.get('Referrer') || alt || '/';
        this.set('Location', url);
        // status
        if (!statuses.redirect[this.status])
            this.status = 302;
        // html
        if (this.ctx.accepts('html')) {
            url = escape(url);
            this.type = 'text/html; charset=utf-8';
            this.body = `Redirecting to <a href="${url}">${url}</a>.`;
            return;
        }
        // text
        this.type = 'text/plain; charset=utf-8';
        this.body = `Redirecting to ${url}.`;
    },
    /**
     * Set Content-Disposition header to "attachment" with optional `filename`.
     *
     * @param {String} filename
     * @api public
     */
    attachment(filename, options) {
        if (filename)
            this.type = extname(filename);
        this.set('Content-Disposition', contentDisposition(filename, options));
    },
    /**
     * Set Content-Type response header with `type` through `mime.lookup()`
     * when it does not contain a charset.
     *
     * Examples:
     *
     *     this.type = '.html';
     *     this.type = 'html';
     *     this.type = 'json';
     *     this.type = 'application/json';
     *     this.type = 'png';
     *
     * @param {String} type
     * @api public
     */
    set type(type) {
        type = getType(type);
        if (type) {
            this.set('Content-Type', type);
        }
        else {
            this.remove('Content-Type');
        }
    },
    /**
     * Set the Last-Modified date using a string or a Date.
     *
     *     this.response.lastModified = new Date();
     *     this.response.lastModified = '2013-09-13';
     *
     * @param {String|Date} type
     * @api public
     */
    set lastModified(val) {
        if ('string' == typeof val)
            val = new Date(val);
        this.set('Last-Modified', val.toUTCString());
    },
    /**
     * Get the Last-Modified date in Date form, if it exists.
     *
     * @return {Date}
     * @api public
     */
    get lastModified() {
        const date = this.get('last-modified');
        if (date)
            return new Date(date);
    },
    /**
     * Set the ETag of a response.
     * This will normalize the quotes if necessary.
     *
     *     this.response.etag = 'md5hashsum';
     *     this.response.etag = '"md5hashsum"';
     *     this.response.etag = 'W/"123456789"';
     *
     * @param {String} etag
     * @api public
     */
    set etag(val) {
        if (!/^(W\/)?"/.test(val))
            val = `"${val}"`;
        this.set('ETag', val);
    },
    /**
     * Get the ETag of a response.
     *
     * @return {String}
     * @api public
     */
    get etag() {
        return this.get('ETag');
    },
    /**
     * Return the response mime type void of
     * parameters such as "charset".
     *
     * @return {String}
     * @api public
     */
    get type() {
        const type = this.get('Content-Type');
        if (!type)
            return '';
        return type.split(';', 1)[0];
    },
    /**
     * Check whether the response is one of the listed types.
     * Pretty much the same as `this.request.is()`.
     *
     * @param {String|Array} types...
     * @return {String|false}
     * @api public
     */
    is(types) {
        const type = this.type;
        if (!types)
            return type || false;
        if (!Array.isArray(types))
            types = [].slice.call(arguments);
        return typeis(type, types);
    },
    /**
     * Return response header.
     *
     * Examples:
     *
     *     this.get('Content-Type');
     *     // => "text/plain"
     *
     *     this.get('content-type');
     *     // => "text/plain"
     *
     * @param {String} field
     * @return {String}
     * @api public
     */
    get(field) {
        return this.header[field.toLowerCase()] || '';
    },
    /**
     * Set header `field` to `val`, or pass
     * an object of header fields.
     *
     * Examples:
     *
     *    this.set('Foo', ['bar', 'baz']);
     *    this.set('Accept', 'application/json');
     *    this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
     *
     * @param {String|Object|Array} field
     * @param {String} val
     * @api public
     */
    set(field, val) {
        if (this.headerSent)
            return;
        if (2 == arguments.length) {
            if (Array.isArray(val))
                val = val.map(v => typeof v === 'string' ? v : String(v));
            else if (typeof val !== 'string')
                val = String(val);
            this.res.setHeader(field, val);
        }
        else {
            for (const key in field) {
                this.set(key, field[key]);
            }
        }
    },
    /**
     * Append additional header `field` with value `val`.
     *
     * Examples:
     *
     * ```
     * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
     * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
     * this.append('Warning', '199 Miscellaneous warning');
     * ```
     *
     * @param {String} field
     * @param {String|Array} val
     * @api public
     */
    append(field, val) {
        const prev = this.get(field);
        if (prev) {
            val = Array.isArray(prev)
                ? prev.concat(val)
                : [prev].concat(val);
        }
        return this.set(field, val);
    },
    /**
     * Remove header `field`.
     *
     * @param {String} name
     * @api public
     */
    remove(field) {
        if (this.headerSent)
            return;
        this.res.removeHeader(field);
    },
    /**
     * Checks if the request is writable.
     * Tests for the existence of the socket
     * as node sometimes does not set it.
     *
     * @return {Boolean}
     * @api private
     */
    get writable() {
        // can't write any more after response finished
        if (this.res.finished)
            return false;
        const socket = this.res.socket;
        // There are already pending outgoing res, but still writable
        // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
        if (!socket)
            return true;
        return socket.writable;
    },
    /**
     * Inspect implementation.
     *
     * @return {Object}
     * @api public
     */
    inspect() {
        if (!this.res)
            return;
        const o = this.toJSON();
        o.body = this.body;
        return o;
    },
    /**
     * Return JSON representation.
     *
     * @return {Object}
     * @api public
     */
    toJSON() {
        return only(this, [
            'status',
            'message',
            'header'
        ]);
    },
    /**
     * Flush any set headers, and begin the body
     */
    flushHeaders() {
        this.res.flushHeaders();
    }
};
/**
 * Custom inspection implementation for newer Node.js versions.
 *
 * @return {Object}
 * @api public
 */
if (util.inspect.custom) {
    exports[util.inspect.custom] = exports.inspect;
}
